前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片NEW
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 业务场景题真实业务问题与追问
  • 查漏补缺常见问题解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
    • AI 定制路线NEW按你的简历现排
    • AI 知识地图NEW串起全站知识点
  • 动态
    • AI 热点NEWAI 每日动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
AI 助手NEW
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片NEW
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 业务场景题真实业务问题与追问
  • 查漏补缺常见问题解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
    • AI 定制路线NEW按你的简历现排
    • AI 知识地图NEW串起全站知识点
  • 动态
    • AI 热点NEWAI 每日动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
AI 助手NEW
旧版
  • Vue

    • 从源码解读Vue生命周期
    • 组件的本质
    • 有状态组件的设计
    • 设计 VNode
    • 辅助创建 VNode 的 h 函数
    • 自定义渲染器和异步渲染
    • 渲染器之挂载
    • 渲染器的核心 Diff 算法
    • 渲染器之patch
    • 图解 Vue 响应式原理
    • 图解 Vue 异步更新
    • 剖析 Vue 内部运行机制
    • vue响应式原理模拟
    • vue状态管理之vuex
    • 理解Vue的设计思想及实现Vue
    • diff算法深入
    • vue router vuex原理分析
    • Vue3初探响应式原理.
    • vue2源码分析
    • vue组件化实践
  • React

  • Webpack

  • Node

  • Javascript

  • 综合

完整面试题地址:
作者:程序员poetry
扫码关注作者公众号:「前端进阶之旅」 每天分享技术干货
前端进阶之旅公众号二维码

ReactDOM.render 是如何串联渲染链路的|原理篇

文章学习导航
面试总览

30 秒速记

  • 核心判断:Fiber 把不可中断的递归遍历改造成可保存进度的工作单元,使渲染阶段能够调度、暂停和恢复
  • 原理主线:围绕 「一、ReactDOM.render 调用」、「二、初始化阶段」、「三、render 阶段」 建立输入、状态变化与输出之间的因果关系
  • 文章范围:解析了 ReactDOM.render 在 Fiber 架构下的渲染流程,结合源码讲解首次渲染时的初始化、render 阶段和 commit 阶段的执行机制,帮助读者全面理解 React 渲染链路的原理和调用栈分层。
  • 边界与代价:可中断的是渲染计算,提交阶段仍需保持一致性;React 18+ 并发能力也不等于所有更新都并行执行
  • 工程落地:优化时先减少无效工作和长任务,再结合优先级、过渡更新和切片机制改善响应性

ReactDOM.render 会依次串起根节点初始化、render 阶段构建 Fiber 树,以及 commit 阶段更新真实 DOM。 首次渲染先创建 fiberRoot 和作为虚拟树根节点的 rootFiber,再由 updateContainer 创建更新、写入队列并调用 scheduleUpdateOnFiber。在旧版 legacy 模式下,这条链路会进入 performSyncWorkOnRoot 同步完成渲染,最后通过 commitRoot 提交结果。需要注意这是历史实现,React 19 已移除该 API,当前入口应使用 createRoot。

这篇文章不要按 API 清单来背。先用上面的 Mind Map 建立全局结构,再通过交互 DEMO 观察正常路径和边界路径如何改变状态;阅读正文时重点核对每一步的输入、负责执行的参与者、产生的中间状态以及最终可观察结果。遇到版本敏感结论,要把“历史实现”“当前行为”和“工程兼容策略”分开说明;遇到性能或架构取舍,则用实际指标、失败现象和验证手段支撑判断。

版本校准: 本文若分析 ReactDOM.render、旧生命周期或栈调和,应把它视为理解架构演进的历史路径。React 19 已移除 ReactDOM.render,当前客户端入口使用 createRoot;并发渲染也必须区分可中断的渲染阶段与同步提交阶段。迁移前对照 React 19 官方升级指南。

从本讲开始,我们将以首次渲染为切入点,拆解 Fiber 架构下 ReactDOM.render 所触发的渲染链路,结合源码理解整个链路中所涉及的初始化、render 和 commit 等过程

# 一、ReactDOM.render 调用栈的逻辑分层

开篇先给到你一个简单的 React AppDemo:

import React from "react";
import ReactDOM from "react-dom";

function App() {
    return (
      <div className="App">
        <div className="container">
          <h1>我是标题</h1>
          <p>我是第一段话</p>
          <p>我是第二段话</p>
        </div>
      </div>
    );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
@前端进阶之旅: 代码已经复制到剪贴板

Demo 启动后,渲染出的界面如下图所示:

现在请你打开 Chrome 的 Performance 面板,点击下图红色圈圈所圈住的这个“记录”按钮:

然后重新访问 Demo 页面对应的本地服务地址,待页面刷新后,终止记录,便能够得到如下图右下角所示的这样一个调用栈大图:

放大该图,定位“src/index.js”这个文件路径,我们就可以找到 ReactDOM.render 方法对应的调用栈,如下图所示:

从图中你可以看到,ReactDOM.render 方法对应的调用栈非常深,中间涉及的函数量也比较大。如果这张图使你心里发虚,请先不要急于撤退——分析调用栈只是我们理解渲染链路的一个手段,我们的目的是借此提取关键逻辑,而非理解调用栈中的每一个方法。就这张图来说,你首先需要把握的,就是整个调用链路中所包含的三个阶段

图中 scheduleUpdateOnFiber 方法的作用是调度更新,在由 ReactDOM.render 发起的首屏渲染这个场景下,它触发的就是 performSyncWorkOnRoot。performSyncWorkOnRoot 开启的正是我们反复强调的 render 阶段;而 commitRoot 方法开启的则是真实 DOM 的渲染过程(commit 阶段)。因此以 scheduleUpdateOnFiber 和 commitRoot 两个方法为界,我们可以大致把 ReactDOM.render 的调用栈划分为三个阶段:

  • 初始化阶段
  • render 阶段
  • commit 阶段

# 二、初始化阶段

# 拆解 ReactDOM.render 调用栈——初始化阶段

首先我们提取出初始化过程中涉及的调用栈大图:

图中的方法虽然看上去又多又杂,但做的事情清清爽爽,那就是完成 Fiber 树中基本实体的创建。

什么是基本实体?基本实体有哪些?问题的答案藏在源码里,这里我为你提取了源码中的关键逻辑,首先是 legacyRenderSubtreeIntoContainer 方法。在 ReactDOM.render 函数体中,以下面代码所示的姿势调用了它:

return legacyRenderSubtreeIntoContainer(null, element, container, false, callback);
@前端进阶之旅: 代码已经复制到剪贴板

而 legacyRenderSubtreeIntoContainer 的关键逻辑如下(解析在注释里):

function legacyRenderSubtreeIntoContainer(parentComponent, children, container, forceHydrate, callback) {
  // container 对应的是我们传入的真实 DOM 对象
  var root = container._reactRootContainer;
  // 初始化 fiberRoot 对象
  var fiberRoot;
  // DOM 对象本身不存在 _reactRootContainer 属性,因此 root 为空
  if (!root) {
    // 若 root 为空,则初始化 _reactRootContainer,并将其值赋值给 root
    root = container._reactRootContainer = legacyCreateRootFromDOMContainer(container, forceHydrate);
    // legacyCreateRootFromDOMContainer 创建出的对象会有一个 _internalRoot 属性,将其赋值给 fiberRoot
    fiberRoot = root._internalRoot;

    // 这里处理的是 ReactDOM.render 入参中的回调函数,你了解即可
    if (typeof callback === 'function') {
      var originalCallback = callback;
      callback = function () {
        var instance = getPublicRootInstance(fiberRoot);
        originalCallback.call(instance);
      };
    } // Initial mount should not be batched.
    // 进入 unbatchedUpdates 方法
    unbatchedUpdates(function () {
      updateContainer(children, fiberRoot, parentComponent, callback);
    });
  } else {
    // else 逻辑处理的是非首次渲染的情况(即更新),其逻辑除了跳过了初始化工作,与楼上基本一致
    fiberRoot = root._internalRoot;
    if (typeof callback === 'function') {
      var _originalCallback = callback;
      callback = function () {
        var instance = getPublicRootInstance(fiberRoot);
        _originalCallback.call(instance);
      };
    } // Update

    updateContainer(children, fiberRoot, parentComponent, callback);
  }
  return getPublicRootInstance(fiberRoot);
}
@前端进阶之旅: 代码已经复制到剪贴板

这里我为你总结一下首次渲染过程中 legacyRenderSubtreeIntoContainer 方法的主要逻辑链路:

在这个流程中,你需要关注到 fiberRoot 这个对象。fiberRoot 到底是什么呢?这里我将运行时的 root 和 fiberRoot 为你截取出来,其中 root 对象的结构如下图所示:

可以看出,root 对象(container._reactRootContainer)上有一个 _internalRoot 属性,这个 _internalRoot 也就是 fiberRoot。fiberRoot 的本质是一个 FiberRootNode 对象,其中包含一个 current 属性,该属性同样需要划重点。这里我为你高亮出 current 属性的部分内容:

或许你会对 current 对象包含的海量属性感到陌生和头大,但这并不妨碍你 Get 到“current 对象是一个 FiberNode 实例”这一点,FiberNode,正是 Fiber 节点对应的对象类型。current 对象是一个 Fiber 节点,不仅如此,它还是当前 Fiber 树的头部节点

考虑到 current 属性对应的 FiberNode 节点,在调用栈中实际是由 createHostRootFiber 方法创建的,React 源码中也有多处以 rootFiber 代指 current 对象,因此下文中我们将以 rootFiber 指代 current 对象。

读到这里,你脑海中应该不难形成一个这样的指向关系:

其中,fiberRoot 的关联对象是真实 DOM 的容器节点;而 rootFiber 则作为虚拟 DOM 的根节点存在。这两个节点,将是后续整棵 Fiber 树构建的起点。

接下来,fiberRoot 将和 ReactDOM.render 方法的其他入参一起,被传入 updateContainer 方法,从而形成一个回调。这个回调,正是接下来要调用的 unbatchedUpdates 方法的入参。我们一起看看 unbatchedUpdates 做了什么,下面代码是对 unbatchedUpdates 主体逻辑的提取:

← 如何理解 Fiber 架构的迭代动机与设计思想剖析 Fiber 架构下 Concurrent 模式的实现原理 →

fe
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片NEW
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 业务场景题真实业务问题与追问
  • 查漏补缺常见问题解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
    • AI 定制路线NEW按你的简历现排
    • AI 知识地图NEW串起全站知识点
  • 动态
    • AI 热点NEWAI 每日动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
AI 助手NEW
旧版
  • Vue

    • 从源码解读Vue生命周期
    • 组件的本质
    • 有状态组件的设计
    • 设计 VNode
    • 辅助创建 VNode 的 h 函数
    • 自定义渲染器和异步渲染
    • 渲染器之挂载
    • 渲染器的核心 Diff 算法
    • 渲染器之patch
    • 图解 Vue 响应式原理
    • 图解 Vue 异步更新
    • 剖析 Vue 内部运行机制
    • vue响应式原理模拟
    • vue状态管理之vuex
    • 理解Vue的设计思想及实现Vue
    • diff算法深入
    • vue router vuex原理分析
    • Vue3初探响应式原理.
    • vue2源码分析
    • vue组件化实践
  • React

  • Webpack

  • Node

  • Javascript

  • 综合